﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml;
using System.IO;
using LockCrypt.Core;

namespace LockCrypt.DAL {
    /// <summary>
    /// Uses an Xml Database to store LockCrypt data.
    /// </summary>
    public class XmlProvider : FileBasedDatabaseProvider {
        private readonly IEncryptionService _encryptionService;
        private bool? _isConnected;
        
        /// <summary>
        /// Gets the encryption provider.
        /// </summary>
        /// <value>The encryption provider.</value>
        public override IEncryptionService Encryption {
            get { return _encryptionService; }
        }

        /// <summary>
        /// Gets the type of database which this provider can access.
        /// </summary>
        /// <value>The type of database which this provider can access.</value>
        public override Constants.DatabaseType DatabaseType {
            get { return Constants.DatabaseType.XML; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is either connected or can connect.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is connected or can connect, otherwise <c>false</c>.
        /// </value>
        public override bool IsConnected {
            get {
                if(!_isConnected.HasValue) {
                    _isConnected = CheckPassword();
                }
                return _isConnected.Value;
            }
        }

        /// <summary>
        /// Gets whether an attempt can be made to open a database connection.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if an attempt can be made to open a database connection, otherwise <c>false</c>.
        /// </value>
        public override bool CanAttemptConnection {
            get { return File.Exists(FileName); }
        }

        /// <summary>
        /// Gets the database connection string.
        /// </summary>
        /// <value>The connection string.</value>
        public override string ConnectionString {
            get { return FileName; }
        }

        /// <summary>
        /// Gets a formatted description of this database connection.
        /// </summary>
        /// <value>A formatted description of this database connection.</value>
        public override string FormattedName {
            get {
                try {
                    return string.Format("xml://{0}", Path.GetFullPath(FileName));
                } catch {
                    return string.Format("xml://{0}", FileName);
                }
            }
        }

        /// <summary>
        /// Initializes a new <see cref="XmlProvider"/> for a specified file, using the current encryption service and key.
        /// </summary>
        /// <param name="fileName">Name of the database file.</param>
        public XmlProvider(string fileName) 
            :base(fileName) {
            _encryptionService = new NullEncryptionService();
        }

        /// <summary>
        /// Initializes a new <see cref="XmlProvider"/> for a specified file
        /// </summary>
        /// <param name="fileName">Name of the database file.</param>
        /// <param name="encryptionService">The encryption service.</param>
        public XmlProvider(string fileName, IEncryptionService encryptionService)
            : this(fileName) {
            _encryptionService = encryptionService;
        }

        #region Accounts
        /// <summary>
        /// Adds accounts to the default database.
        /// </summary>
        /// <param name="accounts">The accounts to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        public override void AddAccounts(IEnumerable<IAccount> accounts, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newIds = db.Accounts.Select(a => a.ID);
            db.Accounts.AddRange(accounts.Where(a => !newIds.Contains(a.ID)));
            ImportDatabase(db, logger);
        }

        /// <summary>
        /// Edits an account.
        /// </summary>
        /// <param name="account">The account.</param>
        public override void EditAccount(IAccount account) {
            var db = ReadDatabase();
            db.Accounts.RemoveAll(a => a.ID == account.ID);
            db.Accounts.Add(account);
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Deletes an account.
        /// </summary>
        /// <param name="accountId">The account ID.</param>
        public override void DeleteAccount(string accountId) {
            var db = ReadDatabase();
            db.Accounts.RemoveAll(a => a.ID == accountId);
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Deletes multiple accounts.
        /// </summary>
        /// <param name="accountIds">The account IDs.</param>
        public override void DeleteAccounts(IEnumerable<string> accountIds) {
            var db = ReadDatabase();
            db.Accounts.RemoveAll(a => accountIds.Contains(a.ID));
            ImportDatabase(db, null);
        }

        public override List<IAccount> GetAccounts() {
            XDocument db = ReadDatabaseDocument();
            IEnumerable<XElement> templateElements = db.Element("lockcrypt").Element("templates").Elements("template");
            List<ITemplate> templates = new List<ITemplate>();
            foreach(var element in templateElements) {
                try {
                    ITemplate template = TemplateBase.FromXElement(element);
                    templates.Add(template);
                } catch(Exception ex) {
                    _logger.Log("Error loading template: " + ex.Message);
                }
            }

            IEnumerable<XElement> accountElements = db.Element("lockcrypt").Element("accounts").Elements("account");
            List<IAccount> accounts = new List<IAccount>();
            foreach(var element in accountElements) {
                try {
                    IAccount account = AccountBase.FromXElement(element);
                    var template = templates.FirstOrDefault(t => t.ID == account.Template.ID);
                    if(template != null)
                        account.Template = template;
                    accounts.Add(account);
                } catch(Exception ex) {
                    _logger.Log("Error loading account: " + ex.Message);
                }
            }
            return accounts;
        }
        #endregion

        #region Groups
        /// <summary>
        /// Adds account groups to the default database.
        /// </summary>
        /// <param name="groups">The groups to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        public override void AddGroups(IEnumerable<IGroup> groups, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newGroupIds = db.Groups.Select(g=>g.ID);
            db.Groups.AddRange(groups.Where(g=> !newGroupIds.Contains(g.ID)));
            ImportDatabase(db, logger);
        }

        /// <summary>
        /// Edits a group.
        /// </summary>
        /// <param name="group">The group.</param>
        public override void EditGroup(IGroup group) {
            var db = ReadDatabase();
            db.Groups.RemoveAll(g => g.ID == group.ID);
            db.Groups.Add(group);
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Deletes a group and all child groups/accounts.
        /// </summary>
        /// <param name="group">The group.</param>
        public override void DeleteGroup(IGroup group) {
            var db = ReadDatabase();
            IEnumerable<IGroup> nestedGroups = LockCryptBLL.ArrangeGroups(db.Groups);
            IGroup groupToDelete = null; // find the group in the heirarchy
            foreach(var nestedGroup in nestedGroups) {
                // search top level groups
                groupToDelete = nestedGroup.FindGroup(group.ID);
                if(groupToDelete != null)
                    break;
            }
            IEnumerable<IGroup> unavailableGroups = (groupToDelete).Flatten();
            var groupIds = unavailableGroups.Select(g => g.ID);

            db.Groups.RemoveAll(g => groupIds.Contains(g.ID));
            db.Accounts.RemoveAll(a => groupIds.Contains(a.ParentID));
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Deletes multiple groups and their child accounts. Does not delete child groups.
        /// </summary>
        /// <param name="groupIds">The group IDs.</param>
        public override void DeleteGroups(IEnumerable<string> groupIds) {
            var db = ReadDatabase();
            db.Groups.RemoveAll(g => groupIds.Contains(g.ID));
            db.Accounts.RemoveAll(a => groupIds.Contains(a.ParentID));
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Gets a list of account groups from the current database.
        /// </summary>
        /// <returns>A list of account groups in the database.</returns>
        public override List<IGroup> GetGroups() {
            XDocument db = ReadDatabaseDocument();
            IEnumerable<XElement> elements = db.Element("lockcrypt").Element("groups").Elements("group");
            List<IGroup> groups = new List<IGroup>();
            foreach(var element in elements) {
                try {
                    IGroup group = GroupBase.FromXElement(element);
                    groups.Add(group);
                } catch(Exception ex) {
                    _logger.Log("Error loading group: " + ex.Message);
                }
            }
            return groups.ToList();
        }
        #endregion

        #region Templates
        /// <summary>
        /// Adds templates to the default database.
        /// </summary>
        /// <param name="templates">The templates to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        /// <returns><c>null</c> on success, a log of errors on failure.</returns>
        public override void AddTemplates(IEnumerable<ITemplate> templates, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newIds = db.Templates.Select(t => t.ID);
            db.Templates.AddRange(templates.Where(t => !newIds.Contains(t.ID)));
            ImportDatabase(db, logger);
        }

        /// <summary>
        /// Edits a template.
        /// </summary>
        /// <param name="template">The template.</param>
        public override void EditTemplate(ITemplate template) {
            var db = ReadDatabase();
            db.Templates.RemoveAll(t => t.ID == template.ID);
            db.Templates.Add(template);
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Deletes a template.
        /// </summary>
        /// <param name="templateId">The template ID.</param>
        public override void DeleteTemplate(string templateId) {
            var db = ReadDatabase();
            db.Templates.RemoveAll(t => t.ID == templateId);
            ImportDatabase(db, null);
        }

        /// <summary>
        /// Gets a list of templates from the current database file.
        /// </summary>
        /// <returns>A list of templates in the database.</returns>
        public override List<ITemplate> GetTemplates() {
            XDocument db = ReadDatabaseDocument();
            IEnumerable<XElement> elements = db.Element("lockcrypt").Element("templates").Elements("template");
            List<ITemplate> templates = new List<ITemplate>();
            foreach(var element in elements) {
                try {
                    ITemplate template = TemplateBase.FromXElement(element);
                    templates.Add(template);
                } catch(Exception ex) {
                    _logger.Log("Error loading template: " + ex.Message);
                }
            }
            return templates.ToList();
        }

        /// <summary>
        /// Gets matching templates by name.
        /// </summary>
        /// <param name="templateName">Name of the template.</param>
        /// <returns>All templates with a matching name.</returns>
        public override List<ITemplate> GetTemplatesByName(string templateName) {
            return GetTemplates().Where(t => t.Name.Equals(templateName, StringComparison.InvariantCultureIgnoreCase)).ToList();
        }
        #endregion

        #region Database Maintenance
        /// <summary>
        /// Checks the password entered is valid.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if the password is valid, otherwise <c>false</c>.
        /// </returns>
        public override bool CheckPassword() {
            try {
                if(string.IsNullOrEmpty(_filename))
                    return false;
                byte[] rawContents = File.ReadAllBytes(_filename);
                string rawXml = string.IsNullOrEmpty(_encryptionService.EncryptionKey) ? Encoding.UTF8.GetString(rawContents) : _encryptionService.DecryptString(rawContents);
                XDocument doc = string.IsNullOrEmpty(rawXml) ? CreateDatabaseDocument(new DatabaseContents()) : XDocument.Parse(rawXml, LoadOptions.None);
                _isConnected = true;
                return true;
            } catch {
                return false;
            }
        }

        /// <summary>
        /// Creates a new database file.
        /// </summary>
        /// <exception cref="System.Exception">Multiple exceptions, check the reference for DirectoryInfo.Create and the FileInfo constructor.</exception>
        public override void CreateDatabase() {
            FileInfo info = new FileInfo(_filename);
            if(!info.Directory.Exists) {
                info.Directory.Create();
            }
            ImportDatabase(new DatabaseContents(), null);
        }

        /// <summary>
        /// Clears the database.
        /// </summary>
        public override void ClearDatabase() {
            ImportDatabase(new DatabaseContents(), null);
        }

        /// <summary>
        /// Reads the whole contents of the database.
        /// </summary>
        /// <returns>The whole contents of the database.</returns>
        public override DatabaseContents ReadDatabase() {
            XDocument doc = ReadDatabaseDocument();
            XElement root = doc.Element("lockcrypt");

            DatabaseContents contents = new DatabaseContents();
            IEnumerable<XElement> accountElements = root.Element("accounts").Elements("account");
            foreach(var element in accountElements) {
                try {
                    contents.Accounts.Add(AccountBase.FromXElement(element));
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }

            IEnumerable<XElement> templateElements = root.Element("templates").Elements("template");
            foreach(var element in templateElements) {
                try {
                    contents.Templates.Add(TemplateBase.FromXElement(element));
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }

            IEnumerable<XElement> groupElements = root.Element("groups").Elements("group");
            foreach(var element in groupElements) {
                try {
                    contents.Groups.Add(GroupBase.FromXElement(element));
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }
            //_logger.Log(string.Format("{0} accounts, {1} groups, {2} templates", contents.Accounts.Count, contents.Groups.Count, contents.Templates.Count));
            return contents;
        }

        /// <summary>
        /// Changes the encryption password.
        /// </summary>
        /// <param name="newPassword">The new password.</param>
        public override void ChangePassword(string newPassword) {
            var db = ReadDatabase();
            Encryption.EncryptionKey = newPassword;
            ImportDatabase(db, _logger);
        }

        /// <summary>
        /// Reads a database from the default file.
        /// </summary>
        /// <returns>The current database contents.</returns>
        private XDocument ReadDatabaseDocument() {
            try {
                byte[] rawContents = File.ReadAllBytes(_filename);
                string rawXml = string.IsNullOrEmpty(_encryptionService.EncryptionKey) ? Encoding.UTF8.GetString(rawContents) : _encryptionService.DecryptString(rawContents);
                XDocument doc = string.IsNullOrEmpty(rawXml) ? CreateDatabaseDocument(new DatabaseContents()) : XDocument.Parse(rawXml, LoadOptions.None);
                return doc;
            } catch(Exception Exception) {
                throw new LockCryptException(I18nUtils.GetString("Errors", "ErrorLoadingDatabase") + " " + Exception.Message, 232);
            }
        }

        /// <summary>
        /// Gets an XDocument for a set of contents database.
        /// </summary>
        /// <returns>An XDocument containing <c>contents</c>.</returns>
        private XDocument CreateDatabaseDocument(DatabaseContents contents) {
            XDocument doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"));
            XElement root = new XElement("lockcrypt");
            doc.Add(root);
            XElement meta = new XElement("meta",
                new XElement("generator", "LockCrypt (.NET) " + Constants.GetVersion().ToString(3)),
                new XElement("date", DateTime.Now.ToUniversalTime().ToString("s")));
            root.Add(meta);

            int numItems = 0;
            XElement xAccounts = new XElement("accounts"); // pwlist -> pwentry in v <= 2.31
            root.Add(xAccounts);
            foreach(var account in contents.Accounts) {
                try {
                    xAccounts.Add(account.ToXElement());
                    numItems++;
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }
            //_logger.Log(string.Format("Read {0} accounts", numItems), StatusMessage.StatusMessageType.Success);

            numItems = 0;
            XElement xTemplates = new XElement("templates"); // pwlist -> type in v <= 2.31
            root.Add(xTemplates);
            foreach(var template in contents.Templates) {
                try {
                    xTemplates.Add(template.ToXElement());
                    numItems++;
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }
            //_logger.Log(string.Format("Read {0} templates", numItems), StatusMessage.StatusMessageType.Success);

            numItems = 0;
            XElement xGroups = new XElement("groups"); // non-existant in v <= 2.31
            root.Add(xGroups);
            foreach(var group in contents.Groups) {
                try {
                    xGroups.Add(group.ToXElement());
                    numItems++;
                } catch(Exception ex) {
                    _logger.Log(ex.Message, true);
                }
            }
            //_logger.Log(string.Format("Read {0} groups", numItems), StatusMessage.StatusMessageType.Success);

            return doc;
        }

        /// <summary>
        /// Imports a database without clearing existing data.
        /// </summary>
        /// <param name="contents">The contents to import.</param>
        /// <param name="logger">The logger.</param>
        public override void ImportDatabase(DatabaseContents contents, ILogProvider logger) {
            XDocument dbContents = CreateDatabaseDocument(contents);
            byte[] encryptedDatabase = string.IsNullOrEmpty(_encryptionService.EncryptionKey) ? Encoding.UTF8.GetBytes(dbContents.ToString(SaveOptions.None)) : _encryptionService.EncryptString(dbContents.ToString(SaveOptions.None));
            using(FileStream fs = File.Create(_filename)) {
                fs.Close();
            }
            File.WriteAllBytes(_filename, encryptedDatabase);
        }
        #endregion
    }
}
